package cn.remex.core.util;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.Externalizable;
import java.io.IOException;
import java.io.ObjectInput;
import java.io.ObjectOutput;
import java.lang.ref.SoftReference;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

/**
 * Disc: 线程安全的本地缓存,必须定义长度,淘汰使用lru算法(直接使用LinkedHashMap),参考ConcurrentHashMap思路进行分段锁
 * TODO 缓存状态以及命中率维护
 * Created by xinxinran on 16/7/11.
 */
public class LocalCacheWithLRU<K> {

    protected static final Logger logger = LoggerFactory.getLogger(LocalCacheWithLRU.class);

    /*
     * 总size
     */
    private final int size;

    /*
     * 最大size
     */
    private static final int MAX_CAPACITY = 1 << 30;

    /*
     * 最大分段
     */
    private static final int MAX_SEGMENTS = 1 << 16;

    /*
     * 本地缓存存储的LRUMap数组
     */
    private LRUCachedMap<K>[] segments;

    private int segmentMask, segmentShift;

    private LRUCachedMap<K> segmentFor(int hash) {
        return segments[(hash >>> segmentShift) & segmentMask];
    }

    public LocalCacheWithLRU(int size){
        this.size = size;
        initCache(16);
    }

    public LocalCacheWithLRU(int size, int concurrencyLevel){
        this.size = size;
        initCache(concurrencyLevel);
    }

    public void put(String key, K val){
        if(Judgment.nullOrBlank(key) || val == null){
            return;
        }
        int hash = hash(key.hashCode());
        segmentFor(hash).addObject(key, val);
    }

    public K get(String key){
        if(Judgment.nullOrBlank(key)){
            return null;
        }

        int hash = hash(key.hashCode());
        return segmentFor(hash).getObject(key);
    }

    public void remove(String key){
        if(Judgment.nullOrBlank(key)){
            return;
        }

        int hash = hash(key.hashCode());
        segmentFor(hash).remove(key);
    }

    public synchronized void clearCache() {
        for (LRUCachedMap<K> segment : segments) segment.clear();
    }

    @SuppressWarnings("unchecked")
    private void initCache(int concurrencyLevel){
        int capacity = size;
        if(capacity < 0 ){
            throw new IllegalArgumentException("invalid  capacity:" + capacity);
        }

        if(concurrencyLevel * 4 > capacity){
            throw new IllegalArgumentException("capacity is too small :" + capacity);
        }

        if(concurrencyLevel > MAX_SEGMENTS){
            concurrencyLevel = MAX_SEGMENTS;
        }
        if(capacity > MAX_CAPACITY){
            capacity = MAX_CAPACITY;
        }

        int segSize = 1, sShift = 0;
        while(segSize < concurrencyLevel){
            sShift++;
            segSize = segSize << 1;
        }

        this.segmentShift = 32 - sShift;
        this.segmentMask = segSize - 1;

        this.segments = new LRUCachedMap[segSize];

        int subCount = capacity / segSize;
        if(subCount * segSize < capacity){
            subCount++;
        }

        int segCap = 1;
        while(segCap < subCount){
            segCap = segCap << 1;
        }

        for(int i = 0; i < segSize; i++){
            this.segments[i] = new LRUCachedMap<>(segCap);
        }
    }

    /* ---------------- Small Utilities -------------- */

    /**
     * Applies a supplemental hash function to a given hashCode, which defends
     * against poor quality hash functions. This is critical because
     * ConcurrentHashMap uses power-of-two length hash tables, that otherwise
     * encounter collisions for hashCodes that do not differ in lower or upper
     * bits.
     */
    private static int hash(int h) {
        // Spread bits to regularize both segment and index locations,
        // using variant of single-word Wang/Jenkins hash.
        h += (h << 15) ^ 0xffffcd7d;
        h ^= (h >>> 10);
        h += (h << 3);
        h ^= (h >>> 6);
        h += (h << 2) + (h << 14);
        return h ^ (h >>> 16);
    }

    private static class LRUCachedMap<T> extends LinkedHashMap<String, SoftReference<T>> implements Externalizable{

        private int maxSize;

        //  需要线程安全
        private static final Lock lock = new ReentrantLock();

        public LRUCachedMap() {
            super();
            maxSize = Integer.MAX_VALUE;
        }

        public LRUCachedMap(int size){
            super(size + 1, 1f, true);
            maxSize = size;
        }

        public T addObject(String key, T object){
            try{
                SoftReference<T> object_sr = new SoftReference<T>(object);
                lock.lock();
                put(key, object_sr);
            }finally {
                lock.unlock();
            }
            return object;
        }

        public T getObject(String key){
            SoftReference<T> object_sr;
            try{
                //  因为get也会产生变动操作,所以依然需要加锁
                lock.lock();
                object_sr = get(key);
                if(object_sr == null){
                    return null;
                }
                if(object_sr.get() == null){        //被回收
                    remove(key);
                    return null;
                }
            }finally {
                lock.unlock();
            }
            return object_sr.get();
        }

        /**
         * Removes the mapping for the specified key from this map if present.
         *
         * @param key key whose mapping is to be removed from the map
         * @return the previous value associated with <tt>key</tt>, or
         * <tt>null</tt> if there was no mapping for <tt>key</tt>.
         * (A <tt>null</tt> return can also indicate that the map
         * previously associated <tt>null</tt> with <tt>key</tt>.)
         */
        @Override
        public SoftReference<T> remove(Object key) {
            try {
                lock.lock();
                return super.remove(key);
            }finally {
                lock.unlock();
            }
        }

        /**
         * {@inheritDoc}
         */
        @Override
        public void clear() {
            try {
                lock.lock();
                super.clear();
            }finally {
                lock.unlock();
            }
        }

        /**
         * Returns <tt>true</tt> if this map should remove its eldest entry.
         * This method is invoked by <tt>put</tt> and <tt>putAll</tt> after
         * inserting a new entry into the map.  It provides the implementor
         * with the opportunity to remove the eldest entry each time a new one
         * is added.  This is useful if the map represents a cache: it allows
         * the map to reduce memory consumption by deleting stale entries.
         * <p>
         * <p>Sample use: this override will allow the map to grow up to 100
         * entries and then delete the eldest entry each time a new entry is
         * added, maintaining a steady state of 100 entries.
         * <pre>
         *     private static final int MAX_ENTRIES = 100;
         *
         *     protected boolean removeEldestEntry(Map.Entry eldest) {
         *        return size() &gt; MAX_ENTRIES;
         *     }
         * </pre>
         * <p>
         * <p>This method typically does not modify the map in any way,
         * instead allowing the map to modify itself as directed by its
         * return value.  It <i>is</i> permitted for this method to modify
         * the map directly, but if it does so, it <i>must</i> return
         * <tt>false</tt> (indicating that the map should not attempt any
         * further modification).  The effects of returning <tt>true</tt>
         * after modifying the map from within this method are unspecified.
         * <p>
         * <p>This implementation merely returns <tt>false</tt> (so that this
         * map acts like a normal map - the eldest element is never removed).
         *
         * @param eldest The least recently inserted entry in the map, or if
         *               this is an access-ordered map, the least recently accessed
         *               entry.  This is the entry that will be removed it this
         *               method returns <tt>true</tt>.  If the map was empty prior
         *               to the <tt>put</tt> or <tt>putAll</tt> invocation resulting
         *               in this invocation, this will be the entry that was just
         *               inserted; in other words, if the map contains a single
         *               entry, the eldest entry is also the newest.
         * @return <tt>true</tt> if the eldest entry should be removed
         * from the map; <tt>false</tt> if it should be retained.
         */
        @Override
        protected boolean removeEldestEntry(Map.Entry<String, SoftReference<T>> eldest) {
            return (size() > maxSize);
        }

        /**
         * The object implements the writeExternal method to save its contents
         * by calling the methods of DataOutput for its primitive values or
         * calling the writeObject method of ObjectOutput for objects, strings,
         * and arrays.
         *
         * @param out the stream to write the object to
         * @throws IOException Includes any I/O exceptions that may occur
         * @serialData Overriding methods should use this tag to describe
         * the data layout of this Externalizable object.
         * List the sequence of element types and, if possible,
         * relate the element to a public/protected field and/or
         * method of this Externalizable class.
         */
        @Override
        public void writeExternal(ObjectOutput out) throws IOException {
            int size = size();
            out.writeInt(size);

            if(size > 0){
                for(Map.Entry<String, SoftReference<T>> entry : entrySet()){
                    SoftReference<T> object_sr = entry.getValue();
                    if(object_sr.get() != null){
                        out.writeObject(entry.getKey());
                        out.writeObject(object_sr.get());
                    }
                }
            }
        }

        /**
         * The object implements the readExternal method to restore its
         * contents by calling the methods of DataInput for primitive
         * types and readObject for objects, strings and arrays.  The
         * readExternal method must read the values in the same sequence
         * and with the same types as were written by writeExternal.
         *
         * @param in the stream to read data from in order to restore the object
         * @throws IOException            if I/O errors occur
         * @throws ClassNotFoundException If the class for an object being
         *                                restored cannot be found.
         */
        @SuppressWarnings("unchecked")
        @Override
        public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
            int size = in.readInt(), i = 0;
            try {
                for (i = 0; i < size; i++) {
                    String key = (String)in.readObject();
                    T value = (T) in.readObject();
                    addObject(key, value);
                }
            }catch (Exception e) {
                logger.warn("error occurred while restore LRUCachedMap, restored item size : " + i, e);
            }
        }
    }
}
